#!/usr/bin/python

## shsh2.py
## Author:   Sergio Nasi
## Date:     01 July 2013
## Version:  0.1
## Copyright (c) 2013, Sergio Nasi
## Credits:  James Thiele for writing the initial console.py framework (http://www.eskimo.com/~jet/python/examples/cmd/)

import os
import cmd
import readline
import sys
import socket
import time
import resource
import sqlite3
import urllib2

from prettytable import PrettyTable
from shodan import WebAPI

## Shodan stuff
#  API key
class Console(cmd.Cmd):

    def __init__(self):
        cmd.Cmd.__init__(self)
        self.SHODAN_API_KEY=""
        self.prompt = "shodan> "
        self.intro  = "**********************************\nWelcome to shsh - the SHODAN shell.\n\nHave fun, do no harm.\n**********************************"  ## defaults to None

    ## Command definitions ##
    def do_hist(self, args):
        """Print a list of commands that have been entered"""
        print self._hist

    def do_exit(self, args):
        """Exits from the console"""
        return -1

    ## Command definitions to support Cmd object functionality ##
    def do_EOF(self, args):
        """Exit on system end of file character"""
        return self.do_exit(args)

    def do_shell(self, args):
        """Pass command to a system shell when line begins with '!'"""
        os.system(args)

    def do_help(self, args):
        """Get help on commands
           'help' or '?' with no arguments prints a list of commands for which help is available
           'help <command>' or '? <command>' gives help on <command>
        """
        ## The only reason to define this method is for the help text in the doc string
        cmd.Cmd.do_help(self, args)

    def checkCam(self,ip):
    	try:
    		sock = socket.socket(socket.AF_INET,socket.SOCK_STREAM)
    		sock.settimeout(1.5)
    		sock.connect((ip,80))
    		sock.send('GET /anony/mjpg.cgi HTTP/1.0\r\n\r\n')
    		res = sock.recv(100)
    		if(res.find('200 OK') > 0):
    			return True
    		return False
    	except:
    		return False


    def do_new_query(self,args):
        """Create a new query
           Syntax: new_query <query_name> <search_string>
           Do not use quotes to enclose <search_string>
           
           QUICK REFERENCE (See http://www.shodanhq.com/help/filters)
           after/before:....limit results by date in the format day/month/year (ex. before:20/03/2010)
           city:............name of the city (ex. city:"San Diego")
           country:.........2-letter country code (ex. country:US)
           geo:.............latitude and longitude (ex. geo:50.23,20.06)
           port:............21, 22, 23, 80, 161 or 443
           os:..............operating system (ex. os:Linux)
           net:.............IP range using CIDR notation (ex. net:18.7.7.0/24 )
           hostname:........full or partial host name (ex. hostname:google)
           
           Ex: new_query my_query 'apache country:IT'
        """
        try:
            l = args.split(" ",1)        
            query_name = l[0]
            search_string = l[1]
            
            self._queries[l[0]] = l[1]
            print "Query '%s' created with search string '%s'" % (query_name,search_string)
            
        except:
            print "Error. Syntax is: new_query <query_name> <search_string>\nFor help type: help new_query"
            
    def do_nq (self,args):
        """Shorcut for new_query
        """
        self.do_new_query(args)    
    
    
    def do_list_queries (self,args):
        """ List available queries. Arguments are ignored.
            Syntax: list_queries
        """      
        if len(self._queries)==0:
            print "No query defined yet"
        for i in self._queries:
            print "%s : %s" %(i,self._queries[i])   
        
    def do_lq(self,args):
        """Shorcut for list_queries
        """
        self.do_list_queries(args)


    def do_exec_query (self,args):
        """ Execute a query
            Syntax: exec_query <query_name> [page_number]
            Query must have been created using "new_query". Use "list_queries" to display the list of available queries
            Integer argument [page_number] is optional and defaults to 1
            E.g.: exec_query my_query
        """          
        
        if len(args)==0:
            print "Error. Syntax is: exec_query <query_name> [page_number]\nFor help type: help exec_query"
            return
        
        l = args.split(" ",2)
        if len(l)==1:
            # one argument: query_name
            query_name = l[0]
            page = 1
            if not query_name in self._queries.keys():
                print "Error. Query is not defined. Use 'list_queries' to display the list of available queries"
                return
        elif len(l)==2:
            # two arguments: query_name and page
            query_name = l[0]
            page = l[1]
        
        search_string = self._queries[query_name]               
        print "Executing query %s : '%s' . Getting page %s results.\nPlease wait..." % (query_name,search_string,page)
        
        try:
            start = time.time()
            self._results[query_name] = self.SHODAN.search (search_string,page)
            end = time.time()
            print "Found %d results in %5.2f seconds. Saving page %d results to in-memory database..." % (self._results[query_name]['total'],end-start,page)
        except Exception, err:
            print "Error. Shodan server did not respond: %s" % str(err)
            return    
        
        if self._results[query_name]['total'] != 0:
        
            # store results in sqlite2 memory db
            first = True
            result_set = [()]
        
            # delete older results for same query and same page, if any
            self._conn.executemany ("DELETE FROM shresults WHERE query_name=? AND page=?", [(query_name,page)])
               
            for c in self._results[query_name]['matches']:
                if c['hostnames'] == []:
                    hostname = ['-']
                else:
                    hostname = c['hostnames']
          
                data = c['data']           
                try:
                    serv = socket.getservbyport(c['port'])
                except:
                    serv = "unknown"    

                if first:
                    result_set = [(query_name,page,c['country_name'],c['city'],c['isp'],hostname[0],c['ip'],c['port'],serv,c['os'],data,c['updated'])]
                    first = False
                    continue
                
                result_set.append((query_name,page,c['country_name'],c['city'],c['isp'],hostname[0],c['ip'],c['port'],serv,c['os'],data,c['updated']))
        
            self._conn.executemany ("INSERT INTO shresults values (NULL,?,?,?,?,?,?,?,?,?,?,?,?)",result_set)
        
        print "Done."

    
    def do_xq(self,args):
        """Shorcut for exec_query
        """
        self.do_exec_query(args)


    def do_show_results (self,args):
        """ Show results of a query execution
            Syntax: show_results <query_name> [page_number]
            Query must have been created with "new_query" and populated with "exec_query"
            Argument [page_number] is an interger: if not specified, all result pages are displayed".
        """    
        
        if len(args)==0:
            print "Error. Syntax is: show_results <query_name> [page_number]\nFor help type: help new_query"
            return
            
        l = args.split(" ",3)
        if len(l)==1:
            # one argument: query_name
            query_name = l[0]
            stmt = "SELECT id,page,country,city,isp,hostname,ip,port,service,os,updated FROM shresults WHERE query_name=:query"
            params = {"query":query_name}
            print "Displaying all result pages for query '%s' and search string: '%s'" % (query_name, self._queries[query_name])
        elif len(l)==2:
            # two arguments: query_name and page
            query_name = l[0]
            page = l[1]
            stmt = "SELECT id,page,country,city,isp,hostname,ip,port,service,os,updated FROM shresults WHERE query_name=:query and page=:page ORDER BY page"
            params = {"query":query_name,"page":page}
            print "Displaying PAGE %s results for query '%s' and search string: '%s' " % (page, query_name, self._queries[query_name])
        else:    
            print "Error. Syntax is: show_results <query_name> [page_number]\nFor help type: help new_query"
            return
        
        curs = self._conn.cursor()
        curs.execute(stmt,params)
        headers = [cn[0] for cn in curs.description]
        rows = curs.fetchall()
        
        x = PrettyTable(headers)
        
        for row in rows:
            x.add_row(row)
        
        print (x)
        
        
    def do_sr(self,args):
        """Shorcut for show_results
        """
        self.do_show_results(args)    


    def do_show_column (self,args):
        """ Show one single column from the result set of a query
            Syntax: show_column <query_name> <column_name>
            Query must have been created with "new_query" and populated with "exec_query"
        """
        try:
        
            l = args.split(" ",2)
            query_name = l[0]
            column = l[1]
            stmt = "SELECT " + column + " FROM shresults WHERE query_name=:query"
            curs = self._conn.cursor()
            curs.execute(stmt,{"query":query_name})
            headers = [column]
            rows = curs.fetchall()
        
            x = PrettyTable(headers)
            for row in rows:
                x.add_row(row)
        
            print (x)
                                
        except:
            print "Error. Syntax is: show_column <query_name>\nFor help type: help show_column"
            return


    def do_sc(self,args):
        """Shorcut for show_column
        """
        self.do_show_column(args)
    

    def do_show_response (self,args):
        """ Show the server response of record ID in the result set
            Syntax: show_response <id>
            where <id> should be taken from the output of show_results
        """
        
        if len(args)==0:
            print "Error. Syntax is: show_response <id>\nFor help type: help show_response"
            return
        
        recid = args
        curs = self._conn.cursor()
        curs.execute("SELECT data FROM shresults WHERE id=?",(recid,))
        print curs.fetchone()[0]

    def do_sre (self,args):
        """ Shortcut for show_response
        """
        self.do_show_response(args)

    def do_save_host (self,args):
        """ Save the host information to the database on disk
            Syntax: show_host <id>
            where <id> should be taken from the output of show_results
        """
        
        if len(args)==0:
            print "Error. Syntax is: save_host <id>\nFor help type: help save_host"
            return
        
        recid = args
        curs1 = self._conn.cursor()
        #ask for credentials, if available
        cred = raw_input("Enter credential:(none)")
        if len(cred)==0:
            cred='none'
            
        #enter some notes
        notes = raw_input("Enter a note:(none)")
        if len(notes)==0:
            notes='none'
        
        curs1.execute('''SELECT query_name,
                               country,
                               city,
                               isp,
                               hostname,
                               ip,
                               port,
                               service,
                               os,
                               data,
                               updated
                        FROM shresults WHERE id=?''',(recid,))
        row=curs1.fetchone()
        
        # check if IP is already in vuln.db3
        curs2 = self._vconn.cursor()
        
        curs2.execute('''SELECT count(ip) c FROM vulnerables WHERE ip=?''',(row[5],))
        row2 = curs2.fetchone()
        if row2[0]!=0:
            print "Error: host was already saved into permanent table"
            return
        
        curs2.execute('''INSERT INTO vulnerables (shodan_query,country,city,isp,hostname,ip,port,service,os,credentials,data,notes,shodan_update)
                         VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?)''',
                         (self._queries[row[0]],row[1],row[2],row[3],row[4],row[5],row[6],row[7],row[8],cred,row[9],notes,row[10]))
        
        self._vconn.commit()


    def do_sh (self,args):
        """Shorcut for save_host
        """
        self.do_save_host(args)        



    def do_webcamcheck (self,args):
        """ Run a test for open webcam on host
            Syntax: webcamcheck <id>
            where <id> identifies a single record in the output of show_results or
            a range of id's in the format: start-end.
            Examples:
                webcamcheck 5
                webcamcheck 10-20
            
        """
        try:
            
            curs = self._conn.cursor()
            
            id=-1
            l = args.split("-",1)        
            if len(l)==1:
                id = l[0]
                curs.execute ("SELECT id,ip,isp,hostname FROM shresults WHERE id=?",(id,))
            elif len(l)==2:
                lo = l[0]
                hi = l[1]
                curs.execute ("SELECT id,ip,isp,hostname FROM shresults WHERE id>=? and id<=?",(lo,hi))

            for row in curs:
                cur_id = row[0]
                cur_ip = row[1]
                print "Checking record id: %d IP: %s" % (cur_id,cur_ip)
                if self.checkCam(cur_ip):
                    print 'Record id %d positive hit. Found http://%s/anony/mjpg.cgi' % (cur_id,cur_ip) 
                
                
        except Exception, err:
            print "Error. Syntax is: webcamcheck <id>\nFor help type: help webcamcheck (%s)" %str(err)

        

    def do_show_mem_usage (self,args):
        """Show memory usage
        """
        cons = resource.getrusage(resource.RUSAGE_SELF)
        print "Resident memory usage: %d bytes" % cons.ru_maxrss

    def do_smu (self,args):
        """ Shortcut for show_mem_usage
        """
        self.do_show_mem_usage(args)


    def do_sql_shell (self,args):
        """ Open a minimal sql shell
            EXPERIMENTAL
        """
        con = self._conn
        
        con.isolation_level = None
        cur = con.cursor()

        buffer = ""

        print "Enter your SQL commands to execute in sqlite3."
        print "Enter a blank line to exit."

        while True:
            line = raw_input()
            if line == "":
                break
            buffer += line
            if sqlite3.complete_statement(buffer):
                try:
                    buffer = buffer.strip()
                    cur.execute(buffer)

                    if buffer.lstrip().upper().startswith("SELECT"):
                        print cur.fetchall()
                except sqlite3.Error, e:
                    print "An error occurred:", e.args[0]
                buffer = ""   
        


    ## Override methods in Cmd object ##
    def preloop(self):
        """Initialization before prompting user for commands.
           Despite the claims in the Cmd documentaion, Cmd.preloop() is not a stub.
        """
        os.system('clear')
        cmd.Cmd.preloop(self)   ## sets up command completion
        self._hist    = []      ## No history yet
        self._locals  = {}      ## Initialize execution namespace for user
        self._globals = {}
        
        try:
            in_file = open(".shodan_api_key","r")
            self.SHODAN_API_KEY = in_file.read()
            in_file.close()
        except Exception, err:
            print "Error reading SHODAN API Key (%s)" % str(err)
            print "Exiting..."
            sys.exit()
        
        self.SHODAN = WebAPI(self.SHODAN_API_KEY)
        self._queries = {}
        self._results = {}
        self._conn = sqlite3.connect(":memory:")
        self._vconn = sqlite3.connect("vuln.db3")
        self._conn.execute('''CREATE TABLE shresults
                     (id INTEGER PRIMARY KEY, 
                      query_name text, 
                      page integer, 
                      country text,
                      city text, 
                      isp text,
                      hostname text, 
                      ip text, 
                      port text, 
                      service text, 
                      os text, 
                      data text, 
                      updated text)''')
                      

    def postloop(self):
        """Take care of any unfinished business.
           Despite the claims in the Cmd documentaion, Cmd.postloop() is not a stub.
        """
        cmd.Cmd.postloop(self)   ## Clean up command completion        
        self._conn.execute ('''DROP TABLE shresults''')
        self._conn.close()
        self._vconn.close()
        print "Exiting..."

    def precmd(self, line):
        """ This method is called after the line has been input but before
            it has been interpreted. If you want to modify the input line
            before execution (for example, variable substitution) do it here.
        """
        self._hist += [ line.strip() ]
        return line

    def postcmd(self, stop, line):
        """If you want to stop the console, return something that evaluates to true.
           If you want to do some post command processing, do it here.
        """
        return stop

    def emptyline(self):    
        """Do nothing on empty input line"""
        pass

    def default(self, line):       
        """Called on an input line when the command prefix is not recognized.
           In that case we execute the line as Python code.
        """
        try:
            exec(line) in self._locals, self._globals
        except Exception, e:
            print e.__class__, ":", e


if __name__ == '__main__':
        console = Console()
        console.cmdloop() 